package xuan.lib;

import java.io.File;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import name.fraser.neil.plaintext.diff_match_patch;
import name.fraser.neil.plaintext.diff_match_patch.Diff;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * @author xuan
 */

public class DocUtils {
	
	/**
	 * get internal tree of one XML file
	 * @param in string path of the file 
	 * @return the internal tree 
	 */
	public static Document getNormalizedDocument(String in) throws Exception{
		
		if(in==null) return null;
		
		Document document=null;
			
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		dbFactory.setNamespaceAware(true);
		dbFactory.setCoalescing(true);
		DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		document = dBuilder.parse(new File(in));
		document.normalize();
		removeEmptyTextNode(document);
		
		return document;		
	}
	
	
	/**
	 * get internal tree of one XML file while hiding inline tags
	 * @param in string path of the file
	 * @return the internal tree
	 */
	public static Document getNormalizedDocumentWithoutInlineTags(String in) throws Exception{
		if(in==null) return null;
		
		Document document=null;
			
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		dbFactory.setNamespaceAware(true);
		dbFactory.setCoalescing(true);
		DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		document = dBuilder.parse(new File(in));
		document.normalize();
		removeEmptyTextNode(document);
		
		//create DOM source				
		Source source = new DOMSource(document);
		
		//create output
		DOMResult result= new DOMResult();
		
		//configure transformer
		String xsl = "xsl/hideInlineTags.xsl";
		TransformerFactory tFactory = TransformerFactory.newInstance();
		StreamSource style = new StreamSource(xsl);
		Transformer transformer = tFactory.newTransformer(style);
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			
		//transformation
		transformer.transform(source, result);
		
		Document output = (Document)result.getNode();
		return output;
	}
	
	/**
	 * @param in
	 * @return
	 */
	public static String createFileWithoutInlineTags(String in) throws Exception{
		if(in==null) return null;
		
		Document document = DocUtils.getNormalizedDocument(in);			
		
		String out = in.substring(in.lastIndexOf('/')); 
		out = out.substring(0,out.lastIndexOf('.'))+"withoutInlineTag"+out.substring(out.lastIndexOf('.'));
		File directory = new File (".");
		out = directory.getCanonicalPath() + "/tmp" + out;
		
		DocUtils.exportXMLFile(document, "xsl/hideInlineTags.xsl", out);
		return out;
	}
	
	/**
	 * @param document
	 * @return
	 */
	public static Document getClonedDocument(Document document){
		if(document==null) return null;
		
		Document newDoc=null;
		try {			 
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
			newDoc = dBuilder.newDocument();			
			newDoc.appendChild(newDoc.adoptNode(document.getDocumentElement().cloneNode(true)));
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return newDoc;		
	}
	
	
	/**
	 * remove empty text node
	 * @param document document underlying
	 * @throws XPathExpressionException
	 */
	public static void removeEmptyTextNode(Document document) throws XPathExpressionException{
		javax.xml.xpath.XPathFactory xpathFactory = XPathFactory.newInstance();
		// XPath to find empty text nodes
		javax.xml.xpath.XPathExpression xpathExp = xpathFactory.newXPath().compile("//text()[normalize-space(.) = '']");  
		NodeList emptyTextNodes = (NodeList) xpathExp.evaluate(document,XPathConstants.NODESET);

		// Remove each empty text node from document.
		for (int i = 0; i < emptyTextNodes.getLength(); i++) {
		    Node emptyTextNode = emptyTextNodes.item(i);
		    emptyTextNode.getParentNode().removeChild(emptyTextNode);
		}
	}

	/**
	 * save internal tree to XML
	 * @param document internal tree
	 * @param fileName file name to save
	 * @throws TransformerException 
	 * @throws IOException
	 */
	public static void exportXMLFile(Document document, String fileName) throws TransformerException, IOException{
		TransformerFactory tFactory = TransformerFactory.newInstance();
//		tFactory.setAttribute("indent-number", 4);
		Transformer transformer = tFactory.newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		DOMSource source = new DOMSource(document);
		File xml = new File(fileName);
		StreamResult result = new StreamResult(xml);
		transformer.transform(source, result);
	}
	
	
	/**
	 * save internal tree to XML using XSL to reactivate CDATA sections
	 * @param document internal tree
	 * @param xsl XSL sheet
	 * @param out file name to save
	 * @throws Exception
	 */
	public static void exportXMLFile(Document document, String xsl, String out) throws Exception{
		//create DOM source				
		Source source = new DOMSource(document);
		
		//create output
		FileOutputStream output = new FileOutputStream(out);
		Result result = new StreamResult(output);
		
		//configure transformer
		TransformerFactory tFactory = TransformerFactory.newInstance();
		StreamSource style = new StreamSource(xsl);
		Transformer transformer = tFactory.newTransformer(style);
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		
		
		//transformation
		transformer.transform(source, result);
	}
	
	
	/**
	 * Display document content
	 * @param root
	 */
	public static void printDoc(Node root){
		if(root==null){			
			return;
		}
		
		if(root.getNodeType() == Node.ELEMENT_NODE){
			System.out.print("<"+root.getNodeName()+" ");
			NamedNodeMap atts = root.getAttributes();
			for(int i=0;i<atts.getLength();i++){
				System.out.print(atts.item(i).getNodeName() + "='" + atts.item(i).getNodeValue() +"'");				
			}
			
			NodeList children = root.getChildNodes();
			if(children.getLength() != 0){
				System.out.println(">");
				for(int i=0;i<children.getLength();i++){
					printDoc(children.item(i));
				}
				
				System.out.println("</"+root.getNodeName()+">");
			}
			else System.out.println("/>");
		}
		else{
			System.out.println(root.getNodeValue());
		}
	}


	/**
	 * convert internal tree to XML string and mark edited node
	 * @param root tree root
	 * @param node1 edited node (inserted, deleted, updated, moved)
	 * @param node2 used to represent move operation
	 * @return
	 */
	public static String XML2String(Node root, Node node1, Node node2){
		if(node1==null) return "";
		String xml="";
		String tagName;
		
		if(root.getNodeType() == Node.ELEMENT_NODE){
			if(root==node1){
				xml = xml + "<changed_zone1>";
			}
			if(node2!=null && root==node2){
				xml = xml + "<changed_zone2>";
			}
			
			tagName = root.getNodeName();
			if(tagName.contains(":")) tagName=tagName.substring(tagName.lastIndexOf(":")+1);
			xml = xml + "<"+tagName;
			
			NodeList children = root.getChildNodes();
			if(children.getLength() != 0){
				xml = xml + ">";
				for(int i=0;i<children.getLength();i++){
					xml = xml + XML2String(children.item(i), node1, node2);
				}
				
				xml = xml + "</"+tagName+">";
			}
			else xml = xml + "/>";
			
			if(root==node1){
				xml = xml + "</changed_zone1>";
			}
			if(node2!=null && root==node2){
				xml = xml + "</changed_zone2>";
			}
		}
		else{
			if(root==node1){
				xml = xml + "<changed_zone1>";
				xml = xml + root.getNodeValue();
				xml = xml + "</changed_zone1>";
			}
			else{
				xml = xml + root.getNodeValue();
			}
		}
		return xml;
	}
	
	/**
	 * convert internal tree to HMTL string
	 * @param root tree root
	 * @param operation
	 * @return
	 */
	public static String XML2HTML(Node root, Operation operation){
		if (operation == null) return "";
		String html;
		
		switch(operation.getType()){
		case INSERT:
		case DELETE:
		case UPDATE:
			html = XML2String(root, operation.getSourceNode(),null);
			break;
		default://MOVE
			html = XML2String(root, operation.getTempNode(),operation.getSourceNode());
			break;
		}		
		
		String pattern = "<(?!(title|para|changed_zone))[A-Za-z][A-Za-z0-9]*>";
//		html = html.replaceAll(pattern, "<div style=\"margin-left:10px; border-left:solid red;\">");
		html = html.replaceAll(pattern, "");
		pattern="</(?!(title|para|changed_zone1|changed_zone2))[A-Za-z][A-Za-z0-9]*>";
//		html = html.replaceAll(pattern, "</div>");
		html = html.replaceAll(pattern, "");
		pattern="<[A-Za-z][A-Za-z0-9]*/>";
		html = html.replaceAll(pattern, "");
		html = html.replace("<title>", "<b>");
		html = html.replace("</title>", "</b><br/>\n");
		html = html.replace("<para>","<p>");
		html = html.replace("</para>","</p><br/>\n");
		
		String linkToChangedDiv;		
		
		switch(operation.getType()){
		case INSERT:
			html = html.replace("<changed_zone1>","<!--changed zone--><div style=\"color:#4e9a06; font-weight:bold\"><a name=\"target\"/>");
			html = html.replace("</changed_zone1>","</div><!--changed zone-->\n");
			linkToChangedDiv = "<div style=\"background-color:#ADD8E6; width=100%; height:20px; text-align:center;\">"+
					"<a href=\"#target\">click on this link to scroll to changed zone</a></div>";
			break;
		case DELETE:
			html = html.replace("<changed_zone1>","<!--changed zone--><div style=\"color:red; text-decoration:line-through;\"><a name=\"target\"/>");
			html = html.replace("</changed_zone1>","</div><!--changed zone-->\n");
			linkToChangedDiv = "<div style=\"background-color:#ADD8E6; width=100%; height:20px; text-align:center;\">"+
					"<a href=\"#target\">click on this link to scroll to changed zone</a></div>";
			break;
		case UPDATE:
			diff_match_patch diffText = new diff_match_patch();
			String source = operation.getSourceNode().getTextContent();
			String target = operation.getMergedNode().getTextContent();
			source = displayInlineTags(source);
			target = displayInlineTags(target);
			LinkedList<Diff> diffs = diffText.diff_main(source, target);
			diffText.diff_cleanupSemantic(diffs);
			html = html.replaceAll("<changed_zone1>.*</changed_zone1>", "<!--changed zone--><div><a name=\"target1\"/>"+diffText.diff_prettyHtml(diffs)+"</div><!--changed zone-->\n");			
			linkToChangedDiv = "<div style=\"background-color:#ADD8E6; width=100%; height:20px; text-align:center;\">"+
			"<a href=\"#target1\">click on this link to scroll to changed zone</a></div>";
			break;
		default://MOVE
			html = html.replace("<changed_zone1>","<!--changed zone--><div style=\"color:#4e9a06; font-weight:bold\"><a name=\"target1\"/>");
			html = html.replace("</changed_zone1>","</div><!--changed zone-->\n");			
			html = html.replace("<changed_zone2>","<!--changed zone--><div style=\"color:red; text-decoration:line-through;\"><a name=\"target2\"/>");
			html = html.replace("</changed_zone2>","</div><!--changed zone-->\n");
			linkToChangedDiv = "<div style=\"background-color:#ADD8E6; width=100%; height:20px; text-align:center;\">"+
						"<a href=\"#target1\">click on this link to scroll to changed zone 1 </a> | "+
						"<a href=\"#target2\">click on this link to scroll to changed zone 2 </a></div>";
			break;
		}
		
		html = "<html><body>\n" + linkToChangedDiv + html + "</body></html>";
	
//		System.out.println(html);
		return html;		
	}

	
	/**
	 * convert text content of a node in internal tree to XML string
	 * @param node current node in tree
	 * @return
	 */
	public static String XML2String(Node node) {
		if(node==null) return "";
		String xml="";
		String tagName;
		if(node.getNodeType() == Node.ELEMENT_NODE){
			tagName = node.getNodeName();
			if(tagName.contains(":")) tagName=tagName.substring(tagName.lastIndexOf(":")+1);
			xml = xml + "<"+tagName;
			
			NodeList children = node.getChildNodes();
			if(children.getLength() != 0){
				xml = xml + ">";
				for(int i=0;i<children.getLength();i++){
					xml = xml + XML2String(children.item(i));
				}
				
				xml = xml + "</"+tagName+">";
			}
			else xml = xml + "/>";
		}
		else{
			xml = xml + node.getNodeValue();
		}
		return xml;
	}
	
	
	/**
	 * convert text content of a node in internal tree to HMTL string
	 * @param node current node in tree
	 * @return
	 */
	public static String XML2HTML(Node node) {
		if (node == null) return "";
		String html = XML2String(node);
		
		String pattern = "<(?!(title|para|changed_zone))[A-Za-z][A-Za-z0-9]*>";
//		html = html.replaceAll(pattern, "<div style=\"margin-left:10px; border-left:solid red;\">");
		html = html.replaceAll(pattern, "");
		pattern="</(?!(title|para|changed_zone1|changed_zone2))[A-Za-z][A-Za-z0-9]*>";
//		html = html.replaceAll(pattern, "</div>");
		html = html.replaceAll(pattern, "");
		html = html.replace("<title>", "<b>");
		html = html.replace("</title>", "</b><br/>");
		html = html.replace("<para>","<p>");
		html = html.replace("</para>","</p><br/>");
//		pattern="(sc:inlineStyle)[^>]*>";
//		html = html.replaceAll(pattern, "b>");
		return html;
	}
	
	/**
	 * display inline tags
	 * @param string
	 * @return
	 */
	public static String displayInlineTags(String string){
		String tmp = string;
		Pattern p = Pattern.compile("<sc:[a-zA-Z]+[^>]*role=\"([^\">]*)\"[^>]*>([^<]*)</sc:[a-zA-Z]+>");
		Matcher m = p.matcher(tmp);
		
		while(m.find()){
			tmp = m.replaceFirst("<"+m.group(1)+">"+m.group(2)+"</"+m.group(1)+">");
			m = m.reset(tmp);
		}
		
		p = Pattern.compile("<sc:inlineImg[^>]*sc:refUri=\"([^\">]*)\"[^>]*/>");
		m = p.matcher(tmp);
	
		while(m.find()){
			tmp = m.replaceFirst("<img src='"+m.group(1)+"'/>");
			m = m.reset(string);
		}
		
		return tmp;
	}
}
